package com.partsoft.dits.exts;

import org.apache.commons.io.IOUtils;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.DecoratingClassLoader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;

import com.partsoft.dits.ConfigurableBean;
import com.partsoft.dits.log.Log;
import com.partsoft.dits.utils.Dom4jUtils;

/**
 * 扩展类加载器
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class ExtendsDirectoryClassLoader extends DecoratingClassLoader implements InitializingBean, ConfigurableBean,
		ExtendsClassPath {

	public static final String EXTENDS_XSD_CLASSPATH = "/com/surekam/dits/dits-extends.xsd";

	public static final String EXTENDS_NAMESPACE_URI = "http://inteldata.surekam.cn/dits/extends";

	public static final String EXTENDS_XMLFILE_CLASSPATH = "META-INF/etc/dits-extends.xml";

	public static final String DEFAULT_EXCLUDED_PACKAGES[] = { "java.", "javax.", "sun.", "oracle." };

	private String extendConfigLocation = EXTENDS_XMLFILE_CLASSPATH;

	private Map<String, JarClassPath> extendsClassPaths = Collections
			.synchronizedMap(new HashMap<String, JarClassPath>(5));

	private Map<String, String> extendsNamespaceJarNames = Collections.synchronizedMap(new HashMap<String, String>(5));

	private Map<String, Map<String, String>> extendsAliasMaps = Collections
			.synchronizedMap(new HashMap<String, Map<String, String>>(5));

	private String librariesLocation;

	private boolean isInited = false;

	private boolean overriding = false;

	private ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver();

	void unLoadJars() {
		synchronized (this) {
			for (Map.Entry<String, JarClassPath> pathEnty : extendsClassPaths.entrySet()) {
				JarClassPath path = pathEnty.getValue();
				if (path instanceof JarClassPath) {
					try {
						path.close();
					} catch (IOException e) {
						Log.error(String.format("关闭扩展包(%s)文件(%s)失败。", pathEnty.getKey(), path.getAbsolutePath()));
					}
				}
			}
			extendsClassPaths.clear();
		}
	}

	protected File getLibrariesDirectory() throws IOException {
		Resource drs = resourceLoader.getResource(getLibrariesLocation());
		if (drs == null)
			throw new IOException(String.format("扩展目录路径(%s)找不到。", getLibrariesLocation()));
		File f = drs.getFile();
		if (f.exists()) {
			if (!f.isDirectory())
				throw new IOException(String.format("扩展配置路径(%s)不是有效的目录名。", getLibrariesLocation()));
		} else
			f.mkdir();
		return f;
	}

	public boolean isOverriding() {
		return overriding;
	}

	public void setOverriding(boolean overriding) {
		this.overriding = overriding;
	}

	private File[] getLibraryFiles(FilenameFilter filter) throws IOException {
		File f = getLibrariesDirectory();
		return filter == null ? f.listFiles() : f.listFiles(filter);
	}

	private Object[] readExtendsClassAliasMap(Document document) {
		String prefixName = Dom4jUtils.resolveDefaultNamespacePrefix(document, EXTENDS_NAMESPACE_URI, "dits");
		Element rootElement = document.getRootElement();
		Node modelsNode = rootElement.selectSingleNode(Dom4jUtils
				.transformXpath("./global-template-models", prefixName));
		Map<String, String> aliasModel = new LinkedHashMap<String, String>();
		Object rets[] = new Object[] { "", aliasModel };
		if (modelsNode == null || !(modelsNode instanceof Element))
			return rets;
		Element modelsElement = (Element) modelsNode;
		String modelNamespace = modelsElement.attributeValue("name");
		modelNamespace = StringUtils.hasText(modelNamespace) ? modelNamespace : "";

		List<Node> childNodes = modelsElement.selectNodes(Dom4jUtils.transformXpath("./template-model", prefixName));
		for (Node methodNode : childNodes) {
			if (!(methodNode instanceof Element))
				continue;
			String methodName = ((Element) methodNode).attributeValue("name");
			String methodClassName = ((Element) methodNode).attributeValue("class");
			if (StringUtils.hasText(methodName) == false || StringUtils.hasText(methodClassName) == false)
				continue;
			try {
				loadClass(methodClassName);
				Assert.isTrue(!aliasModel.containsKey(methodName),
						String.format("别名(%s)已存在相同的实现，类(%s)被忽略。", methodName, methodClassName));
				aliasModel.put(methodName, methodClassName);
			} catch (Throwable e) {
				Log.warn(String.format("定义类(%s)为别名(%s)时发生错误:%s", methodClassName, methodName, e.getMessage()), e);
			}
		}
		return new Object[] { modelNamespace, aliasModel };
	}

	public String getExtendConfigLocation() {
		return StringUtils.hasText(extendConfigLocation) ? extendConfigLocation : EXTENDS_XMLFILE_CLASSPATH;
	}

	public void setExtendConfigLocation(String extendConfigLocation) {
		this.extendConfigLocation = extendConfigLocation;
	}

	private Object[] readExtendsClassAliasMap(InputStream exs) throws IOException {
		ClassPathResource xsdResource = new ClassPathResource(EXTENDS_XSD_CLASSPATH, this.getClass());
		InputStream xsdInputStream = null;
		try {
			try {
				xsdInputStream = xsdResource.getInputStream();
			} catch (IOException e) {
				throw new IllegalStateException(e.getMessage(), e);
			}
			SAXReader saxReader = Dom4jUtils.createSAXReader(xsdInputStream, "utf-8");
			return readExtendsClassAliasMap(saxReader.read(exs));
		} catch (Throwable e) {
			if (xsdInputStream != null) {
				try {
					xsdInputStream.close();
				} catch (IOException ex) {
				}
			}
			throw new IOException(e.getMessage(), e);
		}
	}

	/**
	 * 添加命名类至全局别名空间（必须初始化之后调用）
	 * 
	 * @param namespace
	 *            命名空间
	 * @param aliasClassMap
	 *            别名表
	 */
	private void addGlobalClassAlias(String namespace, Map<String, String> aliasClassMap, String jarName) {
		Assert.notNull(aliasClassMap);
		namespace = StringUtils.hasText(namespace) ? namespace : "";
		synchronized (this) {
			if (extendsAliasMaps.containsKey(namespace)) {
				Log.warn(String.format("别名空间(%s)已存在相同的实现，注册被忽略。", namespace));
			}
			extendsAliasMaps.put(namespace, aliasClassMap);
			extendsNamespaceJarNames.put(jarName, namespace);
		}
	}

	/**
	 * 装载完之后的动作
	 * 
	 * @param jarName
	 * @param jarClassPath
	 */
	protected void afterLoadedClassPath(String jarName, ExtendsClassPath jarClassPath) {
		Assert.notNull(jarClassPath);
		Assert.hasText(jarName);
		URL extendsConfigURL = jarClassPath.findResource(getExtendConfigLocation());
		if (extendsConfigURL == null)
			return;

		InputStream exs = null;
		try {
			exs = extendsConfigURL.openStream();
			Object namedMaps[] = readExtendsClassAliasMap(exs);
			String mapName = (String) namedMaps[0];
			Map<String, String> map = (Map<String, String>) namedMaps[1];
			addGlobalClassAlias(mapName, map, jarName);
		} catch (Throwable e) {
			Log.warn(String.format("读取扩展配置文件(%s)时发生错误。", extendsConfigURL.toString()), e);
		} finally {
			if (exs != null) {
				try {
					exs.close();
				} catch (IOException e) {
				}
			}
		}
	}

	protected boolean acceptClassPath(String jarName, JarClassPath jarClassPath) {
		return true;
	}

	void loadJars() throws IOException {
		synchronized (this) {
			for (File exf : getLibraryFiles(new FilenameFilter() {
				public boolean accept(File dir, String name) {
					name = name.toLowerCase();
					return name.endsWith(".jar") || name.endsWith(".zip");
				}
			})) {
				String tempPackageName = exf.getName().toLowerCase();
				if (!extendsClassPaths.containsKey(tempPackageName)) {
					JarClassPath jarClassPath = new JarClassPath(exf);
					try {
						jarClassPath.open();
					} catch (IOException e) {
						Log.warn(String.format("加载扩展包文件(%s)失败。", exf.getPath()));
						continue;
					}
					if (acceptClassPath(tempPackageName, jarClassPath)) {
						extendsClassPaths.put(tempPackageName, jarClassPath);
						afterLoadedClassPath(tempPackageName, jarClassPath);
					}
				}
			}
		}
	}

	public String getLibrariesLocation() {
		return this.librariesLocation;
	}

	public void setLibrariesLocation(String extsDirPath) {
		this.librariesLocation = extsDirPath;
	}

	private void initExcludePackages() {
		String as[];
		int j = (as = DEFAULT_EXCLUDED_PACKAGES).length;
		for (int i = 0; i < j; i++) {
			String packageName = as[i];
			excludePackage(packageName);
		}
	}

	public ExtendsDirectoryClassLoader() {
		this(Thread.currentThread().getContextClassLoader());
	}

	public ExtendsDirectoryClassLoader(ClassLoader parent) {
		super(parent);
		initExcludePackages();
	}

	@Override
	protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
		Class<?> result = null;
		if (isOverriding()) {
			if (isEligibleForExtends(name)) {
				result = loadClassForExtends(name);
			}
			if (result != null) {
				if (resolve)
					resolveClass(result);
			} else {
				result = super.loadClass(name, resolve);
			}
		} else {
			result = super.loadClass(name, resolve);
		}
		return result;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		Class<?> result = null;
		if (isOverriding()) {
			return super.findClass(name);
		} else {
			try {
				result = super.findClass(name);
				if (result != null)
					return result;
			} catch (ClassNotFoundException e) {
			}
			if (isEligibleForExtends(name))
				result = loadClassForExtends(name);
			if (result == null)
				throw new ClassNotFoundException(name);
		}
		return result;

	}

	protected boolean isEligibleForExtends(String className) {
		return !isExcluded(className);
	}

	protected Class<?> loadClassForExtends(String name) throws ClassNotFoundException {
		Class<?> result = null;
		if (result == null) {
			byte bytes[] = loadBytesForClass(name);
			if (bytes != null)
				result = defineClass(name, bytes, 0, bytes.length);
		}
		return result;
	}

	protected byte[] loadBytesForClass(String name) throws ClassNotFoundException {
		InputStream is = openStreamForClass(name);
		if (is == null)
			return null;
		try {
			byte bytes[] = FileCopyUtils.copyToByteArray(is);
			return transformIfNecessary(name, bytes);
		} catch (IOException ex) {
			throw new ClassNotFoundException((new StringBuilder("Cannot load resource for class [")).append(name)
					.append("]").toString(), ex);
		} finally {
			if (is != null)
				try {
					is.close();
				} catch (IOException e) {
				}
		}
	}

	public InputStream openStreamForClass(String name) {
		for (ExtendsClassPath jarfile : extendsClassPaths.values()) {
			InputStream stream = jarfile.openStreamForClass(name);
			if (stream != null)
				return stream;
		}
		return null;
	}

	@Override
	protected Enumeration findResources(String name) throws IOException {
		Enumeration aenumeration[] = new Enumeration[2];
		aenumeration[0] = super.findResources(name);
		Vector<URL> reses = new Vector<URL>();
		for (ExtendsClassPath jarfile : extendsClassPaths.values()) {
			URL findedURL = jarfile.findResource(name);
			if (findedURL != null)
				reses.add(findedURL);
		}
		aenumeration[1] = reses.elements();

		return new CompoundEnumeration(aenumeration);
	}

	@Override
	public URL findResource(String name) {
		for (ExtendsClassPath jarfile : extendsClassPaths.values()) {
			URL findedURI = jarfile.findResource(name);
			if (findedURI != null)
				return findedURI;
		}
		return super.findResource(name);
	}

	protected byte[] transformIfNecessary(String name, byte bytes[]) {
		return bytes;
	}

	public void afterPropertiesSet() throws Exception {
		initContext();
	}

	public void finiContext() {
		unLoadJars();
		isInited = false;
	}

	public void initContext() {
		if (isAlreadyInit())
			finiContext();
		try {
			loadJars();
		} catch (IOException e) {
			throw new IllegalArgumentException(e.getMessage(), e);
		}
		isInited = true;
	}

	public boolean isAlreadyInit() {
		return isInited;
	}

	/**
	 * 用于继承实现
	 * 
	 * @param packageName
	 * @param classPath
	 * @return true表示可以移除
	 */
	protected boolean canRemoveClassPath(String packageName, ExtendsClassPath classPath) {
		return true;
	}

	/**
	 * 用于继承实现，在移除类路径之前进行一些操作
	 * 
	 * @param packageName
	 *            包名
	 * @param classPath
	 */
	protected void beforeRemoveClassPath(String packageName, ExtendsClassPath classPath) {
		if (extendsNamespaceJarNames.containsKey(packageName)) {
			String namespace = extendsNamespaceJarNames.get(packageName);
			extendsAliasMaps.remove(namespace);
			extendsNamespaceJarNames.remove(packageName);
		}
	}

	File removeExtendsClassPath(String packageName) throws IOException {
		Assert.hasText(packageName);
		packageName = packageName.toLowerCase();
		synchronized (this) {
			JarClassPath decp = extendsClassPaths.get(packageName);
			if (decp == null)
				return null;
			if (!canRemoveClassPath(packageName, decp)) {
				throw new IOException(String.format("不能移除扩展包(%s)。", packageName));
			}
			beforeRemoveClassPath(packageName, decp);
			extendsClassPaths.remove(packageName);
			decp.close();
			return decp.getFile();
		}
	}

	Set<String> getExtendsPackageNames() {
		return Collections.unmodifiableSet(extendsClassPaths.keySet());
	}

	Set<ExtendsLibraryInfomation> getExtendsLibraryInformations() {
		Set<ExtendsLibraryInfomation> ifests = new HashSet<ExtendsLibraryInfomation>(extendsClassPaths.size());
		Map<String, JarClassPath> extendsLibs = new HashMap<String, JarClassPath>(extendsClassPaths);
		for (Map.Entry<String, JarClassPath> jarPath : extendsLibs.entrySet()) {
			ExtendsLibraryInfomation info = new ExtendsLibraryInfomation();
			info.setName(jarPath.getKey());
			InputStream ins = null;
			try {
				URL mfURL = jarPath.getValue().findResource("META-INF/MANIFEST.MF");
				if (mfURL != null) {
					ins = jarPath.getValue().findResource("META-INF/MANIFEST.MF").openStream();
					Properties props = StringUtils.splitArrayElementsIntoProperties(
							(String[]) IOUtils.readLines(ins, "utf-8").toArray(new String[0]), ":");
					info.setSpecificationTitle(props.getProperty(ExtendsLibraryInfomation.ARG_S_TITLE));
					info.setSpecificationVersion(props.getProperty(ExtendsLibraryInfomation.ARG_S_VERSION));
					info.setSpecificationVendor(props.getProperty(ExtendsLibraryInfomation.ARG_S_VENDOR));
					info.setImplementationTitle(props.getProperty(ExtendsLibraryInfomation.ARG_I_TITLE));
					info.setImplementationVersion(props.getProperty(ExtendsLibraryInfomation.ARG_I_VERSION));
					info.setImplementationVendor(props.getProperty(ExtendsLibraryInfomation.ARG_I_VENDOR));
					info.setImplementationURL(props.getProperty(ExtendsLibraryInfomation.ARG_I_URL));
				}
			} catch (Throwable e) {
				Log.warn(String.format("读取%s的[META-INF/MANIFEST.MF]失败。", jarPath.getValue().getAbsolutePath()));
			}
			ifests.add(info);
		}

		return ifests;
	}

	Map<String, Map<String, String>> getExtendsAliasMaps() {
		return extendsAliasMaps;
	}

	Set<String> getExtendsAliasNamespaces() {
		return extendsAliasMaps.keySet();
	}

}
